pacenotes_installer\wizard\scene/
download.rs1use windows_sys::Win32::Foundation::{HWND, LPARAM, WPARAM};
2use windows_sys::Win32::UI::Controls as ctl;
3use windows_sys::Win32::UI::WindowsAndMessaging as wm;
4
5pub unsafe extern "system" fn download_page(
7 dialog: HWND,
8 message: u32,
9 wparam: WPARAM,
10 lparam: LPARAM,
11) -> isize {
12 match message {
13 wm::WM_INITDIALOG => {
14 crate::wizard::page::adopt_state(dialog, lparam);
15 super::download_tree_view::enable_partial_checkboxes(dialog);
16 1
17 }
18 crate::worker::WM_WORKER_DONE => {
19 on_worker_done(dialog, wparam, lparam);
20 1
21 }
22 super::download_tree_view::WM_SYNC_CHECKS => {
23 if let Some(state) = crate::wizard::page::state(dialog) {
24 super::download_tree_view::refresh_checks(dialog, state);
25 }
26 1
27 }
28 wm::WM_NOTIFY => match crate::wizard::page::notify_code(lparam) {
29 Some(ctl::PSN_SETACTIVE) => {
30 crate::wizard::page::set_sheet_caption(dialog);
31 crate::wizard::page::set_header(dialog, crate::i18n::IDS_S4_TITLE);
32 crate::wizard::page::set_buttons(dialog, ctl::PSWIZB_BACK | ctl::PSWIZB_NEXT);
33 crate::wizard::page::set_next_text(dialog, crate::i18n::IDS_BTN_DOWNLOAD);
34 start_catalogue_fetch(dialog);
35 crate::wizard::page::reply(dialog, 0)
36 }
37 Some(super::download_tree_view::TVN_ITEMCHANGEDW) => {
38 super::download_tree_view::on_item_changed(dialog, lparam)
39 }
40 Some(ctl::PSN_WIZNEXT) => on_download(dialog),
41 Some(ctl::PSN_QUERYCANCEL) => crate::wizard::page::query_cancel(dialog),
42 _ => 0,
43 },
44 _ => 0,
45 }
46}
47
48unsafe fn on_worker_done(dialog: HWND, wparam: WPARAM, lparam: LPARAM) {
50 let Some(message) = crate::worker::take(lparam) else {
51 return;
52 };
53 let Some(state) = crate::wizard::page::state(dialog) else {
54 return;
55 };
56 if !state.is_current_request(wparam) {
57 return;
58 }
59 if let crate::worker::Message::Packages(result) = message {
60 match result {
61 Ok(manifest) => {
62 if state.online == Some(false) {
63 state
64 .log
65 .line("the catalogue answered although /health had not");
66 }
67 state.schema_mismatch = schema_mismatch(state, &manifest);
68 build_tree(dialog, state, manifest.packages, None);
69 }
70 Err(reason) => {
71 state
73 .log
74 .line(format!("the catalogue could not be read: {reason}"));
75 let status = if state.online == Some(false) {
77 crate::i18n::IDS_S4_OFFLINE
78 } else {
79 crate::i18n::IDS_S4_FETCH_FAILED
80 };
81 build_tree(dialog, state, Vec::new(), Some(status));
82 }
83 }
84 }
85}
86
87fn schema_mismatch(
90 state: &mut crate::wizard::state::WizardState,
91 manifest: &pacenotes_shared::Manifest,
92) -> bool {
93 let game = state.game_id();
94 match pacenotes_installer::schema::check(game, manifest.schemas.get(game)) {
95 Ok(()) => false,
96 Err(reason) => {
97 state.log.line(describe_schema_mismatch(game, &reason));
98 true
99 }
100 }
101}
102
103fn describe_schema_mismatch(
105 game: &str,
106 reason: &pacenotes_installer::schema::Incompatible,
107) -> String {
108 match reason {
109 pacenotes_installer::schema::Incompatible::NoSchemaPublished => format!(
110 "the server published no install schema for {game}, so this installer cannot \
111 tell whether it understands the catalogue"
112 ),
113 pacenotes_installer::schema::Incompatible::GameUnknown => {
114 format!("this installer has no install schema for {game}")
115 }
116 pacenotes_installer::schema::Incompatible::Different {
117 ours,
118 theirs,
119 unknown_categories,
120 } => {
121 let detail = if unknown_categories.is_empty() {
122 "no category is new, so this is probably only an edit to the schema file"
123 .to_string()
124 } else {
125 format!(
126 "it has categories this installer does not know: {}",
127 unknown_categories.join(", ")
128 )
129 };
130 format!(
131 "the {game} catalogue uses a different install schema ({detail}). \
132 Server {theirs}, this installer {ours}"
133 )
134 }
135 }
136}
137
138unsafe fn start_catalogue_fetch(dialog: HWND) {
143 let Some(state) = crate::wizard::page::state(dialog) else {
144 return;
145 };
146 if state.catalogue_loaded {
147 super::download_tree_view::refresh_checks(dialog, state);
149 return;
150 }
151 state.catalogue_loaded = true;
152
153 let Some(game) = state.game else {
154 build_tree(dialog, state, Vec::new(), Some(crate::i18n::IDS_S4_EMPTY));
155 return;
156 };
157 crate::ui::set_text(
158 dialog,
159 crate::wizard::control_ids::IDC_S4_STATUS,
160 crate::i18n::IDS_S4_LOADING,
161 );
162 let request = state.next_request();
163 let base_url = state.base_url.clone();
164 let token = state.token();
165 let game_id = game.id();
166 crate::worker::spawn(dialog, request, move || {
167 crate::worker::fetch_packages(base_url, token, game_id)
168 });
169}
170
171unsafe fn build_tree(
174 dialog: HWND,
175 state: &mut crate::wizard::state::WizardState,
176 packages: Vec<pacenotes_shared::PackageInfo>,
177 status: Option<u16>,
178) {
179 load_tree(state, packages);
180 super::download_tree_view::populate(dialog, state);
181
182 let status = status.unwrap_or(if state.tree.is_empty() {
183 crate::i18n::IDS_S4_EMPTY
184 } else {
185 crate::i18n::IDS_S4_READY
186 });
187 crate::ui::set_text(dialog, crate::wizard::control_ids::IDC_S4_STATUS, status);
188}
189
190pub(super) fn load_tree(
193 state: &mut crate::wizard::state::WizardState,
194 packages: Vec<pacenotes_shared::PackageInfo>,
195) {
196 let game = state.game_id();
197 let cached = state.cache.entries();
198 let installed: Vec<String> = pacenotes_installer::ledger::Ledger::open(&state.folder)
199 .packages()
200 .keys()
201 .cloned()
202 .collect();
203 state.tree = pacenotes_installer::tree::Tree::build(game, &packages, &cached, &installed);
204 state.packages = packages;
205 state.tree_items = vec![0; state.tree.len()];
207}
208
209unsafe fn on_download(dialog: HWND) -> isize {
213 let Some(state) = crate::wizard::page::state(dialog) else {
214 return crate::wizard::page::reply(dialog, 0);
215 };
216 let choices = pacenotes_installer::schema::choice_categories(state.game_id());
217 let missing = state.tree.categories_missing_a_pick(&choices);
218 if !missing.is_empty() {
219 let text = crate::i18n::load_string_arg(crate::i18n::IDS_S4_MISSING, &missing.join(", "));
220 crate::ui::message_box_warning(dialog, &text);
221 return crate::wizard::page::stay(dialog);
222 }
223 if state.schema_mismatch && !state.tree.checked_needing_download().is_empty() {
224 crate::ui::message_box_warning(
225 dialog,
226 &crate::i18n::load_string(crate::i18n::IDS_S4_NEEDS_NEWER),
227 );
228 return crate::wizard::page::stay(dialog);
229 }
230 state.chosen = state.tree.checked_keys();
231 crate::wizard::page::reply(dialog, 0)
232}